using UnityEngine;

public class MovingSphere : MonoBehaviour
{
    // 最大速度
    [SerializeField, Range(0f, 100f)]
    float maxSpeed = 10f; 
    
    // 最大地面加速度、空中加速度
    [SerializeField, Range(0f, 100f)]
    float maxAcceleration = 10f, maxAirAcceleration = 1f;
    
    // 跳跃高度
    [SerializeField, Range(0f, 10f)]
    float jumpHeight = 2f;
    
    // 允许连续跳跃的次数
    [SerializeField, Range(0, 5)]
    int maxAirJumps = 0;
    
    // 最大地面角度 -- 判断物体是否仍在地面上；楼梯最大角度
    [SerializeField, Range(0f, 90f)]
    float maxGroundAngle = 25f, maxStairsAngle = 50f;
    
    // 可吸附的最大速度 -- 用于将物体吸附到地面
    [SerializeField, Range(0f, 100f)]
    float maxSnapSpeed = 100f;
    
    // 探测距离 -- 用于将物体吸附到地面
    [SerializeField, Min(0f)]
    float probeDistance = 1f;
    
    // 探测层级 -- 用于将物体吸附到地面；楼梯层级
    [SerializeField]
    LayerMask probeMask = -1, stairsMask = -1;
    
    // 输入空间
    [SerializeField]
    Transform playerInputSpace = default;
    
    // 当前的跳跃数
    int jumpPhase;
    
    // 当前速度、期待速度
    Vector3 velocity, desiredVelocity;
    
    // 跳跃
    bool desiredJump;
    
    // 刚体
    Rigidbody body;
    
    // 地面接触点数量；陡峭接触点数量
    int groundContactCount, steepContactCount;

    // 判断是否在地面上
    bool OnGround => groundContactCount > 0;
    
    // 判断是否与峭壁接触
    bool OnSteep => steepContactCount > 0;
    
    // 触碰法线；陡峭接触法线
    Vector3 contactNormal, steepNormal;
    
    // 记录离开地面的帧数，上次跳跃的帧数
    int stepsSinceLastGrounded, stepsSinceLastJump;
    
    // 向上、向右、向前的轴
    Vector3 upAxis, rightAxis, forwardAxis;

    void Awake () {
        body = GetComponent<Rigidbody>();
        OnValidate();
    }
    
    float minGroundDotProduct, minStairsDotProduct;

    void OnValidate () 
    {
        minGroundDotProduct = Mathf.Cos(maxGroundAngle * Mathf.Deg2Rad);
        minStairsDotProduct = Mathf.Cos(maxStairsAngle * Mathf.Deg2Rad);
    }
    void Update () 
    {
        // 读取玩家输入
        Vector2 playerInput;
        playerInput.x = Input.GetAxis("Horizontal");
        playerInput.y = Input.GetAxis("Vertical");
        playerInput = Vector2.ClampMagnitude(playerInput, 1f); // 限制向量模的最大值
        
        if (playerInputSpace) {
            rightAxis = ProjectDirectionOnPlane(playerInputSpace.right, upAxis);
            forwardAxis =
                ProjectDirectionOnPlane(playerInputSpace.forward, upAxis);
        }
        else {
            rightAxis = ProjectDirectionOnPlane(Vector3.right, upAxis);
            forwardAxis = ProjectDirectionOnPlane(Vector3.forward, upAxis);
        }
        desiredVelocity = new Vector3(playerInput.x, 0f, playerInput.y) * maxSpeed; // 更新期望速度
        

        desiredJump |= Input.GetButtonDown("Jump");
    }

    // 更适配物理引擎的更新
    void FixedUpdate()
    {
        upAxis = -Physics.gravity.normalized;
        // 更新状态
        UpdateState();
        
        // 调整速度
        AdjustVelocity();

        // 跳跃
        if (desiredJump) {
            desiredJump = false;
            Jump();
        }
        
        // 更新动量
        body.velocity = velocity;
        
        // 重置状态
        ClearState();
    }
    
    // 重置状态
    void ClearState () {
        groundContactCount = steepContactCount = 0;
        contactNormal = steepNormal = Vector3.zero;
    }
    
    void UpdateState () {
        stepsSinceLastGrounded += 1;
        stepsSinceLastJump += 1;
        velocity = body.velocity;
        if (OnGround || SnapToGround() || CheckSteepContacts()) {
            stepsSinceLastGrounded = 0;
            if (stepsSinceLastJump > 1) {
                jumpPhase = 0;
            }
            if (groundContactCount > 1) {
                contactNormal.Normalize(); // 接触法线 归一化
            }
        }
        else {
            contactNormal = upAxis;
        }
    }
    
    // 判断是否能重新吸附到地面
    bool SnapToGround () {
        if (stepsSinceLastGrounded > 1 || stepsSinceLastJump <= 2) {
            return false;
        }
        
        // 超过可吸附的最大速度
        float speed = velocity.magnitude;
        if (speed > maxSnapSpeed) {
            return false;
        }
        
        // 发射射线
        if (!Physics.Raycast(body.position, -upAxis, out RaycastHit hit, probeDistance, probeMask)) {
            return false;
        }
        
        float upDot = Vector3.Dot(upAxis, hit.normal);
        if (upDot < GetMinDot(hit.collider.gameObject.layer)) {
            return false;
        }
        
        // 重新吸附在地面上
        groundContactCount = 1;
        contactNormal = hit.normal;
        float dot = Vector3.Dot(velocity, hit.normal);
        if (dot > 0f)
        {
            velocity = (velocity - hit.normal * dot).normalized * speed;
        }
        return true;
    }
    
    // 跳跃
    void Jump() {
        Vector3 jumpDirection;
        
        if (OnGround) // 在地面
        {
            jumpDirection = contactNormal;
        }
        else if (OnSteep) // 在峭壁
        {
            jumpDirection = steepNormal;
            jumpPhase = 0;
        }
        else if (maxAirJumps > 0 && jumpPhase <= maxAirJumps) // 空中跳
        {
            if (jumpPhase == 0) {
                jumpPhase = 1;
            }
            jumpDirection = contactNormal;
        }
        else {
            return;
        }
        
        stepsSinceLastJump = 0;
        jumpPhase += 1;
        float jumpSpeed = Mathf.Sqrt(2f * Physics.gravity.magnitude * jumpHeight); // 上升速度

        jumpDirection = (jumpDirection +  upAxis).normalized;
        // 防止连续跳跃时，超过最大上升速度
        float alignedSpeed = Vector3.Dot(velocity, jumpDirection); // 考虑跳跃方向
        if (alignedSpeed > 0f) {
            jumpSpeed = Mathf.Max(jumpSpeed - alignedSpeed, 0f);
        }
        velocity += jumpDirection * jumpSpeed;
        
    }
    
    // 在fixUpdate之后进行物理检测
    void OnCollisionEnter (Collision collision) {
        //onGround = true;
        EvaluateCollision(collision);
    }

    void OnCollisionStay (Collision collision) {
        //onGround = true;
        EvaluateCollision(collision);
    }

    void EvaluateCollision(Collision collision)
    {
        float minDot = GetMinDot(collision.gameObject.layer);
        // 遍历所有接触点
        for (int i = 0; i < collision.contactCount; i++) {
            Vector3 normal = collision.GetContact(i).normal;
            float upDot = Vector3.Dot(upAxis, normal);
            // 依据碰撞点法线判断：球体接触的是地面，而非仅仅是墙壁
            if (upDot >= minDot) {
                groundContactCount += 1;
                contactNormal += normal;
            }
            else if (upDot > -0.01f) {
                steepContactCount += 1;
                steepNormal += normal;
            }
        }
    }
    
    // 将向量投射到平面上
    Vector3 ProjectDirectionOnPlane (Vector3 direction, Vector3 normal) 
    {
        return (direction - normal * Vector3.Dot(direction, normal)).normalized;
    }
    
    // 调整速度，适应斜坡
    void AdjustVelocity () {
        Vector3 xAxis = ProjectDirectionOnPlane(rightAxis, contactNormal);
        Vector3 zAxis = ProjectDirectionOnPlane(forwardAxis, contactNormal);
        
        // 新速度
        float currentX = Vector3.Dot(velocity, xAxis);
        float currentZ = Vector3.Dot(velocity, zAxis);
        
        // 考虑加速度
        float acceleration = OnGround ? maxAcceleration : maxAirAcceleration;
        float maxSpeedChange = acceleration * Time.deltaTime;

        float newX = Mathf.MoveTowards(currentX, desiredVelocity.x, maxSpeedChange);
        float newZ = Mathf.MoveTowards(currentZ, desiredVelocity.z, maxSpeedChange);
        
        velocity += xAxis * (newX - currentX) + zAxis * (newZ - currentZ);
    }
    
    /// <summary>
    /// 获取点积
    /// </summary>
    float GetMinDot (int layer) {
        return (stairsMask & (1 << layer)) == 0 ?
            minGroundDotProduct : minStairsDotProduct;
    }
    
    /// <summary>
    /// 将峭壁接触点转为地面
    /// </summary>
    bool CheckSteepContacts () 
    {
        if (steepContactCount > 1) 
        {
            steepNormal.Normalize();
            float upDot = Vector3.Dot(upAxis, steepNormal);
            if (upDot >= minGroundDotProduct)
            {
                groundContactCount = 1;
                contactNormal = steepNormal;
                return true;
            }
        }
        return false;
    }

}